﻿/*
 * Author: Arron Chapman
 * Author Contact: arron@arronchapman.com
 * Author Website: http://arronchapman.com
 * 
 * Project:MFDrivers
 * Project Website: http://mfdrivers.codeplex.com
 *
 * Creation Date: March 02th, 2012
 *
 * License: Apache 2.0
 * Copyright 2011 Arron Chapman

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
 * 
 * Note: For the purposes of license attribution, keeping the above information in the file is adequate.
 * If you make any modifacations to this file and you think others might benefit from them, please email me at arron@arronchapman.com
 * 
 * 
 * A special thanks goes to CW2, Chris Walker and Unreal-Dude
 * CW2 for providing the answer to all my problems with this driver
 *      http://forums.netduino.com/index.php?/topic/3748-need-some-i2c-help/
 * Chris Walker for his repeated start bit code
 *      http://forums.netduino.com/index.php?/topic/944-i2c-internaladdress-repeated-start-bit-support/
 * Unreal-Dude for loosing almost as much hair as I did trying to help me with it ;)
 * 
 */

/*Part Info
 * Info: 
 * Datasheet: http://ww1.microchip.com/downloads/en/DeviceDoc/21941B.pdf
 *
 */

/*Todo List:
 *  Impliment multi-chip automatic read/write overflow (write 1.25Mb seamlessly spanning 2 chips)
 * 
 */

/*Changelog
 * Date		    | Editor		| Reason
 * 03/02/2012   | Arron Chapman | Initial Draft
 * 03/../2012   | Arron Chapman | Hair pulling
 * 03/16/2012   | Arron Chapman | Working version
 * 03/17/2012   | Arron Chapman | Added block overflow logic
 * 
 * 
 */

using System;
using System.Reflection;
using System.Threading;
using Microsoft.SPOT.Hardware;

namespace MFDrivers.MCP24AA1025
{
    #region pinout info
    #pragma warning disable 1587
    ///
    /// -------=========== IMPORTANT ============----------------
    /// Currently this driver requries Netduino firmware 4.1.1 Beta 1
    /// 
    /// 
    /// 
    /// 
    ///   8  7  6  5
    ///   │  │  │  │ 
    /// █████████████
    /// ▀████████████
    ///   ███████████
    /// ▄████████████
    /// █████████████
    ///   │  │  │  │ 
    ///   1  2  3  4 
    /// 
    /// 1 -> A0 (See note #1 below)
    /// 2 -> A1 (See note #1 below)
    /// 3 -> GND
    /// 4 -> GND
    /// 5 -> SDA (A4)
    /// 6 -> SCL (A5)
    /// 7 -> WP (See note #2 below)
    /// 8 -> 3.3V
    /// 
    /// Notes:
    /// 1) Pins A0 & A1 define the address of the chip, you can use these addresses to put
    ///    up to 4 of these on the same bus. See page 5 of the datasheet for more information.
    /// 2) WP is the Write-Protect pin, when it is tied to GND Writing is permitted, when it 
    ///    is tied to Vcc (+3.3V) the chip cannot be written to.  See page 5 of the datasheet
    ///    for more information.
    ///
    #pragma warning restore 1587
    #endregion

    /// <summary>
    /// A class to read and write to Microchip's 24AA1025 1Mb eeprom
    /// </summary>
    public class MCP24AA1025
    {
        /// <summary>
        /// The I2C port
        /// </summary>
        private static I2CDevice _port;

        /// <summary>
        /// The address of this device
        /// </summary>
        private static byte _address;
        
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="deviceAddress">Device address as defined by the value of pins A0 & A1</param>
        /// <param name="speedKhz"></param>
        public MCP24AA1025(int deviceAddress, int speedKhz = 400)
        {

            switch (deviceAddress)
            {
                case 0:
                    _address = 0x50;
                    break;
                case 1:
                    _address = 0x51;
                    break;
                case 2:
                    _address = 0x82;
                    break;
                case 3:
                    _address = 0x83;
                    break;
                default:
                    throw new ArgumentOutOfRangeException("deviceAddress", "must be 0 - 3");
            }
            _port = new I2CDevice(new I2CDevice.Configuration(_address, speedKhz));
        }

        /// <summary>
        /// Read bytes from the eeprom starting at address
        /// </summary>
        /// <param name="address">Address to start reading from.</param>
        /// <param name="length">The number or bytes to read.</param>
        /// <param name="overflow">If the read length overflows the block boundry should we read from the beginning of next block</param>
        /// <param name="timeout">Timeout to wait for eeprom to ack/nak.</param>
        /// <returns>data returned from eeprom.</returns>
        public byte[] Read(int address, int length = 10, bool overflow = false, int timeout = 1000)
        {
            if((address + length) > 0x1FFFF)
            {
                throw new ArgumentOutOfRangeException("length", "Cannot read " + length +
                                                      " bytes, would extend past end of readable space.");
            }

            if (overflow & (address + length) > 0xFFFF)
            {
                byte[] first = this.Read(address, length, overflow, timeout);
                byte[] second = this.Read(0x1FFFF, length - (0xFFFF - address), overflow, timeout);

                byte[] result = new byte[first.Length + second.Length];
                Array.Copy(first, 0, result, 0, first.Length);
                Array.Copy(second, 0, result, first.Length, second.Length);

                return (result);
            }

            if (address <= 0xFFFF)
            {
                _port.Config = new I2CDevice.Configuration((ushort)(_address & 123 /*1111011*/), _port.Config.ClockRateKhz);
            }
            if (address >= 0x10000)
            {
                _port.Config = new I2CDevice.Configuration((ushort)(_address | 4 /*0000100*/), _port.Config.ClockRateKhz);
            }

            byte[] buffer = new byte[length];
            I2CDevice.I2CReadTransaction transaction = CreateReadTransaction(buffer, (uint)address, 2);

            _port.Execute(new I2CDevice.I2CTransaction[] { transaction }, timeout);

            return buffer;
        }

        /// <summary>
        /// Write bytes to the eeprom starting at address
        /// </summary>
        /// <param name="address">Address to start writing to.</param>
        /// <param name="data">Data to write to the eeprom.</param>
        /// <param name="overflow">Shouold the write operation overflow into the next block if it doesn't fit here.</param>
        /// <param name="wait">Time to wait after writing</param>
        /// <param name="timeout">Timeout to wait for eeprom to ack/nak.</param>
        /// <returns></returns>
        public bool Write(int address, byte[] data, bool overflow = false, int wait = 5, int timeout = 1000)
        {
            if ((address + data.Length) > 0x1FFFF)
            {
                throw new ArgumentOutOfRangeException("data", "Cannot write " + data.Length +
                                                      " bytes, would extend past end of writable space.");
            }

            if(overflow & (address + data.Length) > 0xFFFF)
            {
                byte[] newData = new byte[0xFFFF - address];
                Array.Copy(data, newData, 0xFFFF - address);
                bool first = this.Write(address, data, overflow, wait, timeout);
                bool second = this.Write(0x1FFFF, newData, overflow, wait, timeout);
                return (first && second);
            }

            if(address >= 0x10000)
            {
                _port.Config = new I2CDevice.Configuration((ushort)(_address | 4 /*0000100*/), _port.Config.ClockRateKhz );
            }
            if(address >= 0x1FFFF)
            {
                _port.Config = new I2CDevice.Configuration((ushort)(_address & 123 /*1111011*/), _port.Config.ClockRateKhz);
            }
            
            I2CDevice.I2CWriteTransaction transaction= CreateWriteTransaction(data, (uint)address, 2);

            int bytesWritten = _port.Execute(new I2CDevice.I2CTransaction[] { transaction }, timeout);
            Thread.Sleep(wait);
            return bytesWritten == data.Length;
        }

        /// <summary>
        /// Creates a write transaction with repeat start bit.
        /// Provided by Chris Walker: http://forums.netduino.com/index.php?/topic/944-i2c-internaladdress-repeated-start-bit-support/
        /// </summary>
        /// <param name="buffer">Data to send</param>
        /// <param name="internalAddress">Address to send it to</param>
        /// <param name="internalAddressSize">Length (in bytes) of the address</param>
        /// <returns></returns>
        public static I2CDevice.I2CWriteTransaction CreateWriteTransaction(byte[] buffer, uint internalAddress, byte internalAddressSize)
        {
            I2CDevice.I2CWriteTransaction writeTransaction = I2CDevice.CreateWriteTransaction(buffer);
            Type writeTransactionType = typeof(I2CDevice.I2CWriteTransaction);

            FieldInfo fieldInfo = writeTransactionType.GetField("Custom_InternalAddress", BindingFlags.NonPublic | BindingFlags.Instance);
            fieldInfo.SetValue(writeTransaction, internalAddress);

            fieldInfo = writeTransactionType.GetField("Custom_InternalAddressSize", BindingFlags.NonPublic | BindingFlags.Instance);
            fieldInfo.SetValue(writeTransaction, internalAddressSize);

            return writeTransaction;
        }

        /// <summary>
        /// Creates a write transaction with repeat start bit.
        /// Provided by Chris Walker: http://forums.netduino.com/index.php?/topic/944-i2c-internaladdress-repeated-start-bit-support/
        /// </summary>
        /// <param name="buffer">Buffer to fill</param>
        /// <param name="internalAddress">Address to get data from</param>
        /// <param name="internalAddressSize">Length (in bytes) of the address</param>
        /// <returns></returns>
        public static I2CDevice.I2CReadTransaction CreateReadTransaction(byte[] buffer, uint internalAddress, byte internalAddressSize)
        {
            I2CDevice.I2CReadTransaction readTransaction = I2CDevice.CreateReadTransaction(buffer);
            Type readTransactionType = typeof(I2CDevice.I2CReadTransaction);

            FieldInfo fieldInfo = readTransactionType.GetField("Custom_InternalAddress", BindingFlags.NonPublic | BindingFlags.Instance);
            fieldInfo.SetValue(readTransaction, internalAddress);

            fieldInfo = readTransactionType.GetField("Custom_InternalAddressSize", BindingFlags.NonPublic | BindingFlags.Instance);
            fieldInfo.SetValue(readTransaction, internalAddressSize);

            return readTransaction;
        }
    }
}
